Original version by Jon Lansdell and Nigel Humphreys.
Scripting changes and parameters to script extensions by Greg Sutton.
Human Interface changes and GX Printing by Don Swatman.
Drag and Drop by Chris White.
Introduction
The purpose of the MenuScripter sample code is to demonstrate advanced features of the Open Scripting Architecture (OSA). Using the OSA allows MenuScripter to have its behavior altered by attaching scripts to objects such as a document. The scripts can be created in the Script Editor, MenuScripter itself, or any other script editing application.
MenuScripter 4.0 implements many of the of the techniques described in Paul Smith's develop articles - 'Programming for Flexibility: The Open Scripting Architecture' and 'Implementing Inheritance In Scripts' (see references). If you intend to use the OSA then I recommend that you read these articles.
Here is a list of the OSA features MenuScripter demonstrates:
• Attaching scripts to objects.
• Installing a predispatch Apple event handler to allow an objects script first go at handling any events targetted at the object.
• Directly compiling and executing scripts.
• Decompiling existing scripts for editing.
• Getting and setting properties in scripts.
• Retrieving information on errors that occur in a script.
• Loading and storing scripts.
This version of MenuScripter includes all the scripting updates for 7Edit 3.1, these updates give 7Edit and now MenuScripter scripting abilities similar to the Scriptable Text Editor.
How to use MenuScripter
MenuScripter is basically a text editing application. However, you can also compile and execute the text in any of its document windows. This is similar to compiling and running a script in the Script Editor. 'Compile' and 'Execute' can be found in the 'Script' menu. There is even a results document which displays the result of an executed script or gives details of a errors occurring in a script.
Most menu items have a script associated with them. You can edit these scripts by holding down the control and option keys down as you select a menu item. You will then be presented with a dialog allowing editing of the script. A menu can have one script for all its menu items. This is what the 'Font' menu has.
If a menu item script has a property called 'itemname' then this is used as the text for the menu item. For an example of this take a look at the script for the menu item that shows and hides the results document. This name can be changed through scripting:
tell application "MenuScripter"
set the name of menu item 3 of menu "Script" to "Results Document"
end tell
You can look at the applications script or a document script by getting the appropriate script property as text. The following script will return the applications script:
tell application "MenuScripter"
get script of it as text
end tell
You can set an object's script through scripting also. This is demonstrated in the script for the 'New' menu item of the 'File' menu. AppleScript allows you to create a script object that you can then send to some other application. The following script sets the script of a document:
tell application "MenuScripter"
script aScript
on close aReference saving anEnum
beep
continue close aReference saving anEnum
end close
end script
set script of document 1 to aScript
end tell
This attaches a script containing a 'close' handler to the document object, which will beep then continue with the standard close procedure when the window is closed.
Menu item scripts are treated differently from application and document scripts. Menu item scripts are just executed where as application and document scripts contain command handlers, like the close command handler above, that can handle all the events that the object can itself.
MenuScripter stores its scripts in a 'MenuScripter Prefs' preference file. A documents script is stored as a compiled script in the resource fork of the document file.
Building the Application
MenuScripter compiles under :
Metrowerks CodeWarrior 7 and 8, 68K and PPC
Symantec C++ 8.0.1
Symantec 7.0.4
ETO, MPW Pro #19 'Latest MPW'- Symantec C++ for MPW, MrC
The CodeWarrior project files included are for CodeWarrior 7. To use CodeWarrior 8, replace the CW7 files with the projects in the 'CodeWarrior 8 Projects' folder. Since the PPC ANSI files have changed between CW 7 and 8, the two sets of project files are included.
• Symantec Build Notes
The Symantec environments are using a slightly older version of the Universal Interfaces than MPW and CodeWarrior. You will need to use a later version of the Universal Interfaces than is provided with the Symantec products, or make some changes to the source code. To change the Universal Interfaces, simply place brackets around the existing folder and place the folder containing the later version into the same folder as the existing ones. The brackets will prevent the development environment from using the files contained within the old folder. The later version can be retrieved off the latest ETO CD, it's the 'CIncludes' folder in the Latest MPW:
You will also need to copy another couple of folders into this folder, these are 'GX Libraries' and 'GX Compatibility Interfaces'. These can be found on the latest Mac OS SDK developer CD, here are the paths for the January 1996 CD:
'Dev.CD Jan 96 SDK1:Development Kits (Disc 1):QuickDraw™ GX:Programming Stuff:GX Libraries'
Lastly you need to make sure that you have the 'QuickDrawGXLib.xcoff' library when compiling with Symantec C++ 8.0.1. This should be put in Symantec's 'PPC Libraries' folder and the library can be found from the following path:
It is recommended that you use precompiled headers with Symantec C++ 8.0.1. This will speed compilation and decrease the memory needed. Included in the project is 'Mac #includes.c' to precompile a new 'PPC Macheaders' select this file and select Precompile or Precompile As… from the Build menu.
• MPW Build Notes
A make file to build a 'fat' version of MenuScripter 4.0 using 'Latest MPW' from ETO#19 is included. This make file uses SC and MrC and is designed to compile with the interfaces and libraries from 'Latest MPW'.
To compile MenuScripter 4.0 under MPW you will need to copy the 'GX Libraries' and 'GX Compatibility Interfaces' from the QuickDraw GX release (see notes above for Symantec C++ for Macintosh) into your MPW CIncludes folder. You will also need to copy the version of 'DragLib' included with this project into your MPW:Libraries:Shared Libraries folder. Using older versions of DragLib will produce link errors, since a routine used by MenuScripter 4.0 is not included in earlier versions of DragLib.
Implementation
• Attaching Scripts to Objects
The application and menu item scripts are loaded when the application is launched. This is done in the routine LoadApplicationScripts() in 'MSScript.c'. Scripts are stored in the 'MenuScripter Prefs' preferences file. This is so that changed scripts are stored. Menu scripts are stored as resources with an ID equal to theMenuID * 32 + the item number within the menu. A script works for the whole menu if it has a resource id of 32 * theMenuID. The default scripts are stored as 'SCPT' resources that are not compiled. However in the preferences file they are stored as compiled scripts using 'scpt' resources. BuildMenuScripts() in 'MSScript.c' checks through the preferences file for compiled scripts then through the uncompiled scripts for any that may have been removed or missed.
The default script checks to see whether or not a script already exists for the application and results document. If they do exist then the scripts are not set. The following is a part of the default script that does this:
if not (exists script of it) then
set script of it to ApplicationScript
end if
Currently document scripts are set by the script for the 'New' menu item in the 'File' menu. If you create a document using the 'make' command then the document will not have an associated script unless you include a script for its script property. GetScriptDesc() and SetScriptDesc() in 'MSScript.c' are the routines used to get and set application, document, menu and menu item scripts. They are based off code given in Paul Smith's develop 18 article.
The routine StoreApplicationScripts() in 'MSScript.c' stores the application, results document and menu item scripts when MenuScripter is quit. To try and save time, menu item scripts are only saved to the preferences file if they have changed, or, they contain properties which may change due to running the script.
• Predispatch Apple Event Handler
The predispatch Apple event handler is installed in InitEditorScripting() in 'MSScript.c'. This is based off the handler in Paul Smith's develop 18 article:
pascal OSErr EventPrehandler( AppleEvent *theEvent, AppleEvent *theReply, long theRefcon )
{
// Declare local variables
// Extract the class and ID of the event from the AppleEvent
This handler gets called for every Apple event processed by the application. This allows us to take a look at the class and ID of the event to see if we allow this event to be customized through command handlers in scripts, this is done in CanScriptEvent(). If the event can be scripted then the direct object of the event is resolved to an internal token, seeing as a null descriptor represents the application, if the token is null then the application is assumed to be the direct object.
GetTokenDescScript() looks at the token and checks whether it is the application or a document, in which case it returns the script OSAID of the script attached. If there is no associated script then errAEEventNotHandled is returned and the Apple event is handled in the normal way. Otherwise DoScriptEvent() is called which calls OSADoEvent(). OSADoEvent() looks for a command handler for the event, if there isn't one then the event is passed onto the application in the normal way.
One thing to watch for is being able to cope with optional parameters. If optional parameters are not included in the event but are expected by the command handler then you will get an OSAScriptError. Seeing as you can have only one command handler for each event, that handler needs to be able to take all parameters. It would be nice of the OSA could pattern match the handler to the parameters given, or allow a handler to accept NULL parameters that could be checked on with the 'exists' command. This is not the case at the moment.
The way I have worked around this problem is to add in any missing parameters before passing the event onto a script. This approach is a little messy in that you need to add appropriate descriptors for each parameter for every event that can be handled by a script. However it gives a scripter maximum flexibility in writing command handlers for the application. Taking a look at the make command handler that is in the default application script, this script can be found in the application resources as a 'scpt' resource with an ID of 128:
property newDocuments : 1
on make new aClass at aLocation with data aDataRec with properties aPropertyRec
if aClass is document then
try
name of aPropertyRec
on error
set aName to "untitled"
if newDocuments is not 1 then set aName to aName & " " & newDocuments
set aPropertyRec to aPropertyRec & {name:aName}
end try
set newDocuments to newDocuments + 1
end if
continue make new aClass at aLocation with data aDataRec with properties aPropertyRec
end make
The handler overrides the way MenuScripter allocates names to new untitled documents. If there is no name in the property record then the handler adds one and continues the event. Continuing the event passes the event on to be handled by the application in the normal way. If the handler did not accept all of the parameters then any information passed in that parameter could not be passed on to the application with the continue command, and so would be lost.
If you look at the CanScriptEvent() routine in 'MSScript.c' you can see that it not only checks that the event can be handled by a script command handler, but it also adds any missing optional parameters:
Every object specifier, or reference in AppleScript terms, passed to a command handler has a 'get' event done on it before being given to the command handler as a parameter. This is something to be aware of because it will go through to your application and you need to return the object specifier in many cases.
The reason the 'get' command is called is because some object specifiers will and should resolve to a value before being passed to the command handler. Take the following script example:
tell application "MenuScripter"
make new word at last word of document 1 with data "the end."
end tell
The data parameter, or keyAEData parameter, may not expect an object specfier. So if we look at a more complicated example:
tell application "MenuScripter"
make new document with data (contents of document "Template")
end tell
We would expect that 'contents of document "Template"' would be a value rather than an object specifier. Hence 'get' is used on the object specifier first to get a value.
• Use of ASDebugging Routines
There are some OSA routines in 'ASDebugging.h' that have no documentation other than the prototype in the header file itself. OSAGetProperty() and OSASetProperty() are used in MenuScripter to get and set the name for a menu item. The routines to look at are GetScriptProperty() and SetScriptProperty() in 'MSScript.c'. These routines are called in CheckForMenuItemName() in 'MSAEGetData.c' and SetMenuItemTokenProperty() in 'MSAESetData.c' respectively. The window bounds for a document are stored in the script for a document as a property also. When a document file is opened this property is used to position the window, this is done in OpenOld() in 'MSFile.c':
anErr = noErr; // Script does not have this property
}
Notice that you pass the name of the property as a typeChar descriptor, and that the name is all lower case even if the property contains upper case letters.
The bounds of the document are set while the window is still hidden which can cause problems because the content region of a window is zeroed in a hidden window. Moving and resizing a window is usually associated with the user dragging a visible window, hence the toolbox routine SizeWindow() only works properly on a visible window. To cope with this the window is moved off screen before making the window visible, calling SizeWindow(), then hidden again and moved to the correct position. The code that handles this is in SetWindowTokenProperty() in 'MSAESetData.c'.
There is also a routine I used in debugging called LookAtHandlers() in 'MSScript.c'. This routine uses OSAGetHandlerNames() and OSAGetHandler() to look at all the handlers in a script.
• Errors and Displaying Results
There is now a results document which displays the results of any script executed. It also doubles to display error information when an error occurs in an attached script, or when a document is compiled or executed. The results document is like any other MenuScripter document except that it is not editable. The document is also created when the application is launched and only gets hidden when it is closed.
The three routines to look at are DisplayDescResult(), DisplayOSAIDResult() and DisplayOSAScriptError() in 'MSResultWind.c'. DisplayDescResult() and DisplayOSAIDResult() check for an errOSAScriptError first, if there is an error DisplayOSAScriptError() which calls OSAScriptError() to get specific information on the error. One of the error selectors, kOSAErrorExpectedType, is not actually documented in 'IM - Interapplication Communication' but was useful in debugging the parameters to command handlers.
OSADisplay() is used to convert an OSAID to a form that would be used in AppleScript, or whatever scripting component you are using. This is particularily useful for scriptable applications that allow more than one dialect. The type 'docu' may stir the memory of someone who thinks in English to come up with 'document', but what of those who don't know English? OSADisplay() will convert the OSAID to the current terminology. To convert a descriptor to an OSAID you can use OSACoerceFromDesc(), this is used in TextDescUsingOSADisplay() in 'MSResultWind.c' to display some of the results of OSAScriptError:
If you use typeStyledText with OSADisplay() you will get the text formatted as it would appear in the Script Editor using AppleScript formatting.
Changes to MenuScripter 3.1
Here is a list of the changes to MenuScripter 3.1 to produce MenuScripter 4.0:
• Updated script terminology consistent with 7Edit 3.1.
• Improvements to user interface.
• A results window for displaying the results and errors of scripts.
• Better script error reporting.
• Allowing of scripts to be attached to documents and the application.
• Ability to set and get an objects script through scripting.
• Use of a predispatch Apple event handler to allow an objects script first go at handling an Apple event.
• The 'SCPT' resource storing uncompiled scripts changed to a C string for easier reading and modification.
• Added differentiation between window and document objects.
Future Changes
The way we have implemented the scripting ability and OSA features in MenuScripter are not necessarily the best way. This example is here to show features of the OSA in working code. There are bound to be better ways, or ways that are more suited to your application.
Further things that could be done to improve MenuScripter include:
• Tidy up ambiguity with the use of the word 'text' in scripting.
• There is a little bit of messy window updating to fix.
• Use results document to display any errors when compiling the script of a menu item.
As usual time limits prevent the code from being perfect. If you come across any bugs, have comments or suggestions then send a message to the AppleLink address DEVSUPPORT and mark the link for my attention.